#region Using References

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using Beaker.OpenCube.Mapping;

#endregion

namespace Beaker.OpenCube
{
	/// <summary>
	/// A base class for object of which the properties can be observable, and dynamicly added/removed
	/// </summary>
	public class ObservableExpandoObject
		: DynamicObject, INotifyPropertyChanged, INotifyPropertyChanging
	{
		private Dictionary<string, object> _dictionary;
		private IReadOnlyDictionary<string, object> _readOnlyDictionary;

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryGetMember( GetMemberBinder binder, out object result )
		{
			// no dictionary, no properties, so return false.
			if( _dictionary == null )
			{
				result = null;
				return false;
			}

			return _dictionary.TryGetValue( binder.Name, out result );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TrySetMember( SetMemberBinder binder, object value )
		{
			// Contract.Requires<ArgumentNullException>( binder != null );
			Contract.Ensures( Contract.ValueAtReturn( out value ) == _dictionary[binder.Name] );
			Contract.Ensures( Contract.Result<bool>() == true, "Settings should always succeed" );
			Contract.Ensures( ExpandoProperties.Count > 0, "After setting at least 1 item should be in the dictionary" );

			SetValue( value, binder.Name );

			return true;
		}

		#region Overrides only to hide methods from intellisense

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryBinaryOperation( BinaryOperationBinder binder, object arg, out object result )
		{
			return base.TryBinaryOperation( binder, arg, out result );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryConvert( ConvertBinder binder, out object result )
		{
			return base.TryConvert( binder, out result );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override DynamicMetaObject GetMetaObject( Expression parameter )
		{
			return base.GetMetaObject( parameter );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryCreateInstance( CreateInstanceBinder binder, object[] args, out object result )
		{
			return base.TryCreateInstance( binder, args, out result );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryDeleteIndex( DeleteIndexBinder binder, object[] indexes )
		{
			return base.TryDeleteIndex( binder, indexes );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryDeleteMember( DeleteMemberBinder binder )
		{
			return base.TryDeleteMember( binder );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryGetIndex( GetIndexBinder binder, object[] indexes, out object result )
		{
			return base.TryGetIndex( binder, indexes, out result );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryInvoke( InvokeBinder binder, object[] args, out object result )
		{
			return base.TryInvoke( binder, args, out result );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, out object result )
		{
			return base.TryInvokeMember( binder, args, out result );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TrySetIndex( SetIndexBinder binder, object[] indexes, object value )
		{
			return base.TrySetIndex( binder, indexes, value );
		}

		[EditorBrowsable( EditorBrowsableState.Never )]
		public override bool TryUnaryOperation( UnaryOperationBinder binder, out object result )
		{
			return base.TryUnaryOperation( binder, out result );
		}

		#endregion

		/// <summary>
		/// Sets the property to the given value
		/// </summary>
		/// <param name="expandoPropertyName">Name of the expando property.</param>
		/// <param name="newValue">The new value.</param>
		protected void SetValue( object newValue, [CallerMemberName] string expandoPropertyName = null )
		{
			Contract.Requires<ArgumentNullException>( expandoPropertyName != null, "When calling GetValue from a pre 4.5 compiler, expandoPropertyName should explicitly be set" );
			Contract.Ensures( Contract.ValueAtReturn( out newValue ) == _dictionary[expandoPropertyName] );
			Contract.Ensures( ExpandoProperties.Count > 0, "After setting at least 1 item should be in the dictionary" );

			bool hasChanged;

			// when no dictionary yet, create one
			if( _dictionary == null )
			{
				// create dictionary for first property
				_dictionary = new Dictionary<string, object>();
				_readOnlyDictionary = new ReadOnlyDictionary<string, object>( _dictionary );
				// has Changed is true, becuase this is the first property
				hasChanged = true;
			}
			else
			{
				// test if there was a value already
				object oldValue;
				if( _dictionary.TryGetValue( expandoPropertyName, out oldValue ) )
				{
					// check null references
					if( oldValue == null && newValue == null )
						hasChanged = false; // both null, no change
					else if( oldValue == null || newValue == null )
						hasChanged = true; // one null other not, change
					else
						hasChanged = !oldValue.Equals( newValue ); // change based on comparison of objects
				}
				else
				{
					// new value, always set.
					hasChanged = true;
				}
			}

			// update when value has been changed
			if( hasChanged )
			{
				RaisePropertyChanging( expandoPropertyName );
				_dictionary[expandoPropertyName] = newValue;
				RaisePropertyChanged( expandoPropertyName );
			}
		}

		/// <summary>
		/// Gets the value for the given property.
		/// </summary>
		/// <param name="expandoPropertyName">Name of the expando property.</param>
		/// <returns></returns>
		/// <exception cref="System.MissingMemberException">Thrown when the property does not exist.</exception>
		protected object GetValue( [CallerMemberName] string expandoPropertyName = null )
		{
			Contract.Requires<ArgumentNullException>( expandoPropertyName != null, "When calling GetValue from a pre 4.5 compiler, expandoPropertyName should explicitly be set" );

			if( _dictionary != null )
			{
				object value;
				if( _dictionary.TryGetValue( expandoPropertyName, out value ) )
					return value;
			}

			throw new MissingMemberException( GetType().FullName, expandoPropertyName );
		}

		/// <summary>
		/// Gets the value for the given property, or the default value if the property does not exist.
		/// </summary>
		/// <param name="defaultValue">The default value.</param>
		/// <param name="expandoPropertyName">Name of the expando property.</param>
		/// <returns>The value of the property, or the defualt value if the property does not exist.</returns>
		protected object GetValueOrDefault( object defaultValue, [CallerMemberName] string expandoPropertyName = null )
		{
			Contract.Requires<ArgumentNullException>( expandoPropertyName != null, "When calling GetValue from a pre 4.5 compiler, expandoPropertyName should explicitly be set" );

			if( _dictionary != null )
			{
				object value;
				if( _dictionary.TryGetValue( expandoPropertyName, out value ) )
					return value;
			}

			return defaultValue;
		}

		/// <summary>
		/// Returns the enumeration of all dynamic member names.
		/// </summary>
		/// <returns>
		/// A sequence that contains dynamic member names.
		/// </returns>
		[EditorBrowsable( EditorBrowsableState.Never )]
		public override IEnumerable<string> GetDynamicMemberNames()
		{
			return _readOnlyDictionary.Keys;
		}

		/// <summary>
		/// Returns a read only dictionary of all the properties and there values
		/// </summary>
		/// <returns>A read only dictionary of the expando properties, 
		/// or <c>null</c> when there are no expando properties</returns>
		[Mapping( Ignore = true )]
		public IReadOnlyDictionary<string, object> ExpandoProperties
		{
			get { return _readOnlyDictionary; }
		}

		private PropertyChangingEventHandler _propertyChanging;

		/// <summary>
		/// Occurs before a property will change.
		/// </summary>
		[Mapping( Ignore = true )]
		public event PropertyChangingEventHandler PropertyChanging
		{
			// based on:
			// http://stackoverflow.com/questions/3522361/add-delegate-to-event-thread-safety

			add
			{
				PropertyChangingEventHandler localHandler = _propertyChanging;
				PropertyChangingEventHandler originalHandler;
				do
				{
					originalHandler = localHandler;
					var combinedHandler = (PropertyChangingEventHandler)Delegate.Combine( originalHandler, value );
					localHandler = Interlocked.CompareExchange( ref _propertyChanging, combinedHandler, originalHandler );
				}
				while( localHandler != originalHandler );
			}
			remove
			{
				PropertyChangingEventHandler localHandler = _propertyChanging;
				PropertyChangingEventHandler originalHandler;
				do
				{
					originalHandler = localHandler;
					var combinedHandler = (PropertyChangingEventHandler)Delegate.Remove( originalHandler, value );
					localHandler = Interlocked.CompareExchange( ref _propertyChanging, combinedHandler, originalHandler );
				}
				while( localHandler != originalHandler );
			}
		}

		private PropertyChangedEventHandler _propertyChanged;

		/// <summary>
		/// Occurs after a property has changed.
		/// </summary>
		[Mapping( Ignore = true )]
		public event PropertyChangedEventHandler PropertyChanged
		{
			// based on:
			// http://stackoverflow.com/questions/3522361/add-delegate-to-event-thread-safety

			add
			{
				PropertyChangedEventHandler localHandler = _propertyChanged;
				PropertyChangedEventHandler originalHandler;
				do
				{
					originalHandler = localHandler;
					var combinedHandler = (PropertyChangedEventHandler)Delegate.Combine( originalHandler, value );
					localHandler = Interlocked.CompareExchange( ref _propertyChanged, combinedHandler, originalHandler );
				}
				while( localHandler != originalHandler );
			}
			remove
			{
				PropertyChangedEventHandler localHandler = _propertyChanged;
				PropertyChangedEventHandler originalHandler;
				do
				{
					originalHandler = localHandler;
					var combinedHandler = (PropertyChangedEventHandler)Delegate.Remove( originalHandler, value );
					localHandler = Interlocked.CompareExchange( ref _propertyChanged, combinedHandler, originalHandler );
				}
				while( localHandler != originalHandler );
			}
		}

		/// <summary>
		/// Sets the value of the property and raise NotifyPropertyChanged if changed.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="backingStore">The backing store that holds the value.</param>
		/// <param name="newValue">The new value to set to.</param>
		/// <param name="onChange">The action callback to call if the value actually has changed.</param>
		/// <param name="propertyName">Name of the property.</param>
		protected void SetValue<T>( ref T backingStore, T newValue, Action<T, T> onChange = null, [CallerMemberName] string propertyName = null )
		{
			Contract.Requires( propertyName != null, "A property name is required" );
			Contract.Ensures( Equals( backingStore, newValue ) );

			bool hasChanged = !EqualityComparer<T>.Default.Equals( backingStore, newValue );
			if( hasChanged )
			{
				T oldValue = backingStore;

				RaisePropertyChanging( propertyName );
				backingStore = newValue;
				RaisePropertyChanged( propertyName );

				if( onChange != null )
					onChange( oldValue, newValue );
			}
		}

		/// <summary>
		/// Raises the PropertyChanging event.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		protected virtual void RaisePropertyChanging( [CallerMemberName] string propertyName = null )
		{
			Contract.Requires( propertyName != null, "A property name is required" );

			PropertyChangingEventHandler propertyChanging = _propertyChanging;
			if( propertyChanging != null )
				propertyChanging( this, new PropertyChangingEventArgs( propertyName ) );
		}

		/// <summary>
		/// Raises the PropertyChanged event.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		private void RaisePropertyChanged( [CallerMemberName] string propertyName = null )
		{
			Contract.Requires( propertyName != null, "A property name is required" );

			PropertyChangedEventHandler propertyChanged = _propertyChanged;
			if( propertyChanged != null )
				propertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
		}

		/// <summary>
		/// Raises the PropertyChanging event for the property with the given name.
		/// </summary>
		/// <typeparam name="T">The type of the property that will change.</typeparam>
		/// <param name="propertyExpression">An expression identifying the property that will change.</param>
		protected virtual void RaisePropertyChanging<T>( Expression<Func<T>> propertyExpression )
		{
			RaisePropertyChanging( GetPropertyName( propertyExpression ) );
		}

		/// <summary>
		/// Raises the PropertyChanging event for the property with the given name.
		/// 
		/// </summary>
		/// <typeparam name="T">The type of the property that has changed.</typeparam>
		/// <param name="propertyExpression">An expression identifying the property has changed.</param>
		protected virtual void RaisePropertyChanged<T>( Expression<Func<T>> propertyExpression )
		{
			RaisePropertyChanged( GetPropertyName( propertyExpression ) );
		}

		/// <summary>
		/// Get the name of a property from an expression.
		/// </summary>
		/// <typeparam name="T">The type of the property.</typeparam>
		/// <param name="propertyExpression">An expression returning the property's name.</param>
		/// <returns>
		/// The name of the property returned by the expression.
		/// </returns>
		/// <exception cref="ArgumentNullException">Thrown when the expression is null.</exception>
		/// <exception cref="ArgumentException">Thrown when the expression does not represent a property.</exception>
		protected string GetPropertyName<T>( Expression<Func<T>> propertyExpression )
		{
			if( propertyExpression == null )
				throw new ArgumentNullException( "propertyExpression", "Argument is not a property expression." );

			var memberExpression = propertyExpression.Body as MemberExpression;
			if( memberExpression == null )
				throw new ArgumentException( "Argument is not a property expression.", "propertyExpression" );

			var propertyInfo = memberExpression.Member as PropertyInfo;
			if( propertyInfo == null )
				throw new ArgumentException( "Argument is not a property expression.", "propertyExpression" );

			return propertyInfo.Name;
		}
	}
}